package it.ap.sa.acqua.auth.service;

import java.util.ArrayList;
import java.util.List;

import org.audit4j.core.dto.Field;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import it.ap.sa.acqua.auth.security.CustomUserDetailsService;
import it.ap.sa.core.utils.CoreProperties;


/**
 * La nostra classe personalizzata implementa l'interfaccia AuthenticationProvider di spring security.
 * E il metodo di authenticate viene sovrascritto con la nostra logica.
 * Chiameremo metodo di classe di servizio per caricare l'utente in base al nome utente.
 * Se l'utente non si trova o non corrispondente la password, lancio un'eccezione, il tipo dell'eccezione ci permette di individuare il 
 * messaggio da mostrare in seguito lato web.
 * Se è un utente valido restituire il token di autenticazione.
 * 
 * @author marianna
 */
@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {

	private static final Logger logger = LoggerFactory.getLogger(CustomAuthenticationProvider.class.getName());
	
	@Autowired
	private CustomUserDetailsService userService;
	    
	@Autowired
	private CoreProperties properties;
	
	/**
	 * Performs authentication with the same contract as
	 * {@link org.springframework.security.authentication.AuthenticationManager#authenticate(Authentication)}
	 *
	 * @param authentication the authentication request object.
	 *
	 * @return a fully authenticated object including credentials. May return
	 * <code>null</code> if the <code>AuthenticationProvider</code> is unable to support
	 * authentication of the passed <code>Authentication</code> object. In such a case,
	 * the next <code>AuthenticationProvider</code> that supports the presented
	 * <code>Authentication</code> class will be tried.
	 *
	 * @throws AuthenticationException if authentication fails.
	 */
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		logger.info("Autenticazione ... IN CORSO");

		/** 
		 * Recupero le credenziali di accesso 
		 * **/
		String username = authentication.getName().trim();
		String password = (String) authentication.getCredentials();

		logger.info("Credenziali di accesso: " + username + "/" + password);
		
		List<Field> fields = new ArrayList<Field>();
			fields.add(new Field("Logging username .. ", username));
		
		/**
		 * Controllo che le credenziali di accesso siano valorizzate
		 */
		if(StringUtils.hasText(username) && StringUtils.hasText(password)){
			
			/** Controllo se è attiva l'opzione che permette di bypassare l'autenticazione utente su db **/
			if(!properties.SKIP_UTENTE_LOGGATO){
		
				/** 
				 * Controllo sul db se esiste un utente con questo username 
				 * **/
				UserDetails user = userService.loadUserByUsername(username);
		
				/** L' username non esiste in tabella **/
				if (user == null) {
					
					/****************************************************************************************************/
						fields.add(new Field("Causa", "Non esiste un'utente con questo username."));
					/****************************************************************************************************/
					
					logger.error("Tentativo di autenticazione username " + username + " fallito: In tabella non esiste un'utente con questo username.");
					
					throw new UsernameNotFoundException("Username non trovata");
				}
			
				/**
				 * Esiste un utente con questo username, controllo se la password coincide.
				 * Siccome sul database, la password viene registrata criptandola, per confrontarle crypto allo stesso modo la password 
				 * digitata dall'utente.
				 */
				BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
				
				/** La password digitata non coincide con quella registrata **/
				if (!passwordEncoder.matches(password, user.getPassword())) {
					
					/****************************************************************************************************/
						fields.add(new Field("Causa", "La password digitata non coincide con quella registrata."));
					/****************************************************************************************************/
					
					logger.error("Tentativo di autenticazione username " + username + " fallito: La password digitata non coincide con quella registrata.");
					
					throw new BadCredentialsException("La password digitata non coincide con quella registrata.");
				}
					
				/** L'account utente non è abilitato, non può accedere all'applicazione **/
				if (!user.isEnabled()) {
					
					/****************************************************************************************************/
						fields.add(new Field("Causa", "L'account utente non è abilitato, non può accedere all'applicazione."));
					/****************************************************************************************************/
						
					logger.error("Tentativo di autenticazione username " + username + " fallito: L'account utente non è abilitato, non può accedere all'applicazione.");
					
					throw new DisabledException("L'account utente non è abilitato, non può accedere all'applicazione.");
				}
				
				logger.info("Autenticazione ... TERMINATA");
				
				/****************************************************************************************************/
						fields.add(new Field("User", user.toString()));
						fields.add(new Field("Ruoli", StringUtils.collectionToCommaDelimitedString(user.getAuthorities())));
				/****************************************************************************************************/
					
				/** ritorno il token **/
				return new UsernamePasswordAuthenticationToken(user, password, user.getAuthorities());
			}// close if
			else{
				/** E' attiva l'opzione che permette l'accesso a qualsiasi utente autenticando come Utente di test. **/
				logger.warn("E' attiva l'opzione che permette l'accesso a qualsiasi utente autenticandolo come Utente di test.");
				
				UserDetails user = userService.loadUserByUsernameTest();
				
				return new UsernamePasswordAuthenticationToken(user, password, user.getAuthorities());
			}
		}else
		   /** Una o entrambe le credenziali di accesso non sono valorizzate **/
		   throw new AuthenticationCredentialsNotFoundException("Una o entrambe le credenziali di accesso non sono valorizzate");
	}

	/**
	 * Returns <code>true</code> if this <Code>AuthenticationProvider</code> supports the
	 * indicated <Code>Authentication</code> object.
  	 * <p>
  	 * Returning <code>true</code> does not guarantee an
  	 * <code>AuthenticationProvider</code> will be able to authenticate the presented
  	 * instance of the <code>Authentication</code> class. It simply indicates it can
  	 * support closer evaluation of it. An <code>AuthenticationProvider</code> can still
  	 * return <code>null</code> from the {@link #authenticate(Authentication)} method to
  	 * indicate another <code>AuthenticationProvider</code> should be tried.
  	 * </p>
  	 * <p>
  	 * Selection of an <code>AuthenticationProvider</code> capable of performing
  	 * authentication is conducted at runtime the <code>ProviderManager</code>.
  	 * </p>
  	 *
  	 * @param authentication
  	 *
  	 * @return <code>true</code> if the implementation can more closely evaluate the
  	 * <code>Authentication</code> class presented
  	 */
	@Override
	public boolean supports(Class<?> authentication) {
	    return true;
	}
}
